Reviewer Reproducibility Package
Construction Material Classification from Terrestrial Laser Scanning Using a Reflectance-Related Radiometric Descriptor, Multiscale Geometric Roughness Features, and Automated Machine Learning

======================================================================
1. PURPOSE
======================================================================

This package provides the Python scripts used to reproduce the main
preprocessing, feature-extraction, model-development, held-out evaluation,
and feature-ablation workflow reported in the manuscript.

The complete point-cloud datasets are not included because of their size.
Instead, a small spatially contiguous Painted Surface example is provided:

    Sample_Input_Painted_Surface.txt
    Sample_Output_Painted_Surface.txt

The sample files are intended to demonstrate the expected input/output format
and allow the feature-extraction workflow to be executed on a manageable file.

======================================================================
2. SCRIPT ORDER
======================================================================

Run the scripts in the following order for the full workflow:

00_Filter_Scanning_Distance.py
    Applies the implemented scanner-distance criterion:
        4 m < sqrt(X^2 + Y^2 + Z^2) < 10 m
    Input:
        raw TLS TXT file
    Required columns:
        X Y Z R G B Intensity
    Output:
        distance-filtered raw TLS TXT file

01_CosBR2_Geometric_Factor.py
    Calculates CosBR2, the coordinate-derived geometric factor used in the
    radiometric preprocessing.
    Input:
        filtered raw TLS point cloud
    Output:
        original columns + CosBR2

02_Slope_SKL_Feature_Extraction.py
    Calculates Slope_SKL using local ordinary least-squares regression of
    Intensity on CosBR2.
    Neighbourhood:
        Euclidean radius = 0.10 m
        minimum neighbours = 20
        maximum neighbours = 10,000
    Output:
        original columns + Slope_SKL

03_Ra_Rn_020_Baseline_Feature_Extraction.py
    Calculates the original 0.20 m baseline geometric descriptors:
        Ra_m
        Rn_rad

    Neighbourhood:
        cube side length = 0.20 m
        Chebyshev radius = 0.10 m
        minimum neighbours = 20

    Ra:
        general 3D plane
        RANSAC residual threshold = 0.002 m
        maximum trials = 100
        SVD refit using the best inlier set
        population standard deviation of unsigned orthogonal distances
        from all local neighbours

    Rn:
        local PCA/SVD normals
        theta_ij = arccos(|n_i dot n_j|)
        population standard deviation of angular deviations
        units = radians

04_Multiscale_Ra_Rn_010_030_Feature_Extraction.py
    Adds the additional multiscale descriptors:
        Ra_0p10_m
        Rn_0p10_rad
        Ra_0p30_m
        Rn_0p30_rad

    The existing 0.20 m baseline columns Ra_m and Rn_rad are preserved and
    are not recalculated by this script.

    Cube side lengths:
        0.10 m -> Chebyshev radius 0.05 m
        0.30 m -> Chebyshev radius 0.15 m

    Minimum neighbours:
        20

05_Prepare_Train_and_Balance.py
    Balances the training dataset only.

    Final training protocol:
        7 material classes
        64,296 observations per class
        450,072 observations total
        0.25 m scanning-distance bins
        distance-stratified random undersampling
        sampling without replacement
        master random seed = 42

    The held-out Test dataset is not read, balanced, resampled, or modified
    by this script.

06_FLAML_Final_Model_Training.py
    Trains the final seven-feature model on the balanced Train dataset only.

    Final predictors:
        Slope_SKL
        Ra_0p10_m
        Rn_0p10_rad
        Ra_m
        Rn_rad
        Ra_0p30_m
        Rn_0p30_rad

    FLAML settings:
        time_budget = 900 s
        metric = macro_f1
        task = classification
        eval_method = cv
        n_splits = 3
        seed = 42
        n_jobs = -1
        auto_augment = False
        ensemble = True
        sample = False

    Candidate estimators:
        lgbm
        xgboost
        xgb_limitdepth
        catboost
        histgb
        rf
        extra_tree

    The held-out Test dataset is not used during model selection,
    cross-validation, or hyperparameter optimisation.

07_Held_Out_Test_Evaluation.py
    Loads the frozen final model and evaluates it once on the untouched
    held-out Test dataset.

    Reported outputs include:
        accuracy
        balanced accuracy
        macro precision
        macro recall
        macro F1
        weighted precision
        weighted recall
        weighted F1
        per-class precision/recall/F1/support
        raw confusion matrix
        row-normalised confusion matrix

    No retraining, balancing, tuning, or threshold optimisation is performed
    on the held-out Test dataset.

08_Ablation_S1_S3_S4.py
    Reproduces:
        S1 = Slope_SKL only
        S3 = Slope_SKL + multiscale Ra
        S4 = Slope_SKL + multiscale Ra + multiscale Rn

    S1 and S3 are trained using the same FLAML protocol as the final model.
    S4 uses the already frozen final seven-feature model.

09_Ablation_S2_Ra020.py
    Reproduces:
        S2 = Slope_SKL + Ra_m at 0.20 m

    Uses the same balanced Train dataset, untouched held-out Test dataset,
    and FLAML optimisation protocol.

======================================================================
3. SAMPLE FILES
======================================================================

Sample_Input_Painted_Surface.txt

    Contains 300 spatially contiguous TLS points with:
        X Y Z R G B Intensity

Sample_Output_Painted_Surface.txt

    Contains the corresponding 300 sample points after feature extraction:
        X
        Y
        Z
        R
        G
        B
        Intensity
        CosBR2
        Slope_SKL
        Ra_m
        Rn_rad
        Ra_0p10_m
        Rn_0p10_rad
        Ra_0p30_m
        Rn_0p30_rad

The sample output was recalculated from the 300-point sample itself using the
provided feature-extraction functions. It is therefore an execution example
for this sample, rather than a direct 300-row extraction from the full
production feature file. Local geometric features can depend on neighbouring
points outside a cropped sample in the full scan.

======================================================================
4. FULL-DATA TRAIN/TEST STRUCTURE
======================================================================

The final analysis used separate Train and held-out Test datasets acquired
from different scanner positions and spatially distinct material regions.
No point samples were shared between the Train and Test datasets.

The final unbalanced point counts were:

Material                    Train        Test
------------------------------------------------
Asphalt                     274,748      254,336
Carpet                       98,945       69,883
Ceiling Tile                126,294       97,700
Concrete                    242,938      396,597
Laminated MDF Board         233,770      327,162
Painted Surface             100,131       92,131
PVC Vinyl Tile               64,296       59,013
------------------------------------------------
Total                     1,141,122    1,296,822

Only the Train dataset was balanced before FLAML model development.

======================================================================
5. FINAL MODEL FEATURES
======================================================================

The final predictive model uses exactly seven predictors:

    Slope_SKL
    Ra_0p10_m
    Rn_0p10_rad
    Ra_m
    Rn_rad
    Ra_0p30_m
    Rn_0p30_rad

Material, Split, scanner distance, XYZ coordinates, RGB values, Intensity,
and CosBR2 are not supplied to the final classifier as predictive features.

======================================================================
6. SOFTWARE ENVIRONMENT
======================================================================

The final model-development environment reported in the study used:

    Python          3.13.2
    FLAML           2.3.6
    scikit-learn    1.7.0

The scripts also require, as applicable:

    numpy
    pandas
    scipy
    joblib
    scikit-learn
    flaml
    lightgbm
    xgboost
    catboost

Tkinter is used for graphical file/folder selection in the supplied scripts.

A separate requirements.txt file can be used to record the package versions
for installation.

======================================================================
7. REPRODUCIBILITY NOTES
======================================================================

1. All scripts should be run on copies of the source files.

2. The Train and held-out Test datasets must remain separate throughout model
   development and evaluation.

3. The held-out Test dataset must not be balanced or resampled.

4. The 0.20 m Ra/Rn features are generated first. The 0.10 m and 0.30 m
   descriptors are subsequently added while preserving the 0.20 m values.

5. PCA/SVD normal vectors have arbitrary sign. Rn therefore uses the
   orientation-invariant angular relation:
       arccos(|n_i dot n_j|)

6. Negative Slope_SKL values are valid outputs of the local regression and
   should not be removed solely because they are negative.

7. The AutoML search is time-budgeted and uses parallel processing. Exact
   wall-clock time can vary by hardware. The supplied random seeds and
   software-version information are provided to improve reproducibility.

8. The complete datasets may be provided separately subject to the data
   availability statement in the manuscript.

======================================================================
8. EXPECTED FINAL HELD-OUT TEST RESULT
======================================================================

Using the frozen final stacking model and the untouched held-out Test dataset
(N = 1,296,822), the reported overall metrics were approximately:

    Accuracy              90.4353%
    Balanced accuracy     87.4182%
    Macro precision       88.5373%
    Macro recall          87.4182%
    Macro F1              87.8453%
    Weighted precision    90.6966%
    Weighted recall       90.4353%
    Weighted F1           90.4058%

These results represent scan-position-separated held-out validation within
the same general study area and should not be interpreted as evidence of
cross-site generalisation.
